package de.hsb.ai.devil.agent;

import jade.content.Concept;
import jade.content.ContentElement;
import jade.content.lang.Codec.CodecException;
import jade.content.onto.OntologyException;
import jade.content.onto.basic.Action;
import jade.content.onto.basic.Result;
import jade.core.AID;
import jade.core.Agent;
import jade.domain.FIPAAgentManagement.NotUnderstoodException;
import jade.domain.FIPAAgentManagement.RefuseException;
import jade.domain.introspection.EventRecord;
import jade.domain.introspection.Occurred;
import jade.lang.acl.ACLMessage;
import jade.lang.acl.MessageTemplate;
import jade.proto.SimpleAchieveREInitiator;
import jade.proto.SimpleAchieveREResponder;

import java.util.HashMap;
import java.util.Map;
import java.util.NoSuchElementException;

import org.apache.log4j.Logger;

import de.hsb.ai.devil.domain.DevilVocabulary;
import de.hsb.ai.devil.domain.GameInfo;
import de.hsb.ai.devil.domain.action.GetDelta;
import de.hsb.ai.devil.domain.action.StartGame;
import de.hsb.ai.devil.domain.event.GameOver;
import de.hsb.ai.devil.util.DevilHelper;

/**
 * DevilMasterAgent masters games between two players.
 */
@SuppressWarnings("serial")
public class DevilMasterAgent extends AbstractDevilAgent {
	
	public static final String SERVICE_TYPE = "devil-master";
	
	public static final Logger logger = Logger.getLogger(SERVICE_TYPE);
	
	private Map<String, GameInfo> games = new HashMap<String, GameInfo>();
	
	public DevilMasterAgent() {
		super(SERVICE_TYPE, SERVICE_TYPE);
	}
	
	@Override
	protected void setup() {
		super.setup();
		MessageTemplate mt = MessageTemplate.and(
				DevilHelper.createMessageTemplate(ACLMessage.PROPOSE), 
				MessageTemplate.MatchReplyWith(DevilVocabulary.GAME_START));
		// Wait for orders
		addBehaviour(new AwaitGame(this, mt));
	}
	
	/**
	 * Creates an event message for {@link GameOver}. Both players a setup as receivers.
	 * @param winner Agent who won
	 * @param id Game/conversation ID
	 * @return the setup message
	 * @throws CodecException
	 * @throws OntologyException
	 * @throws NoSuchElementException if {@code id} is not associated with a {@link GameInfo}
	 */
	private ACLMessage createGameOverEvent(AID winner, String id) throws CodecException, OntologyException {
		ACLMessage msg = DevilHelper.createDevilRequest(this, winner, ACLMessage.INFORM);
		
		GameInfo sg = games.get(id);
		if (sg == null)
			throw new NoSuchElementException("unkown id: " + id);
		
		AID loser = winner.equals(sg.getPlayerA()) ? sg.getPlayerB() : sg.getPlayerA();
		msg.addReceiver(loser);
		
		Occurred occ = new Occurred();
		GameOver go = new GameOver();
		go.setWinner(winner);
		go.setMessage("Thank you, it was a pleasure playing with you");
		occ.setWhat(new EventRecord(go, null));
		
		getContentManager().fillContent(msg, occ);
		
		return msg;
	}
	
	/**
	 * Creates a request message for {@link GetDelta}.
	 * @param receiver Agent to be asked
	 * @param id Game/conversation ID
	 * @return the setup message
	 * @throws CodecException
	 * @throws OntologyException
	 * @throws NoSuchElementException if {@code id} is not associated with a {@link GameInfo} 
	 */
	private ACLMessage createDeltaRequest(AID receiver, String id) throws CodecException, OntologyException {
		ACLMessage msg = DevilHelper.createDevilRequest(this, receiver, ACLMessage.REQUEST);
		msg.setConversationId(id);
		
		GameInfo sg = games.get(id);
		if (sg == null)
			throw new NoSuchElementException("unkown id: " + id);
		
		GetDelta gd = new GetDelta();
		gd.setTotal(sg.getTotal());
		
		Action actionWrapper = new Action(receiver, gd);
		getContentManager().fillContent(msg, actionWrapper);
		return msg;
	}
	
	/**
	 * Adds a new {@link RequestDelta RequestDelta-Behavior} to this agent's behaviors.
	 * @param from Agent to be asked
	 * @param id Game/conversation ID
	 * @throws CodecException
	 * @throws OntologyException
	 */
	private void requestDelta(AID from, String id) throws CodecException, OntologyException {
		addBehaviour(new RequestDelta(this, createDeltaRequest(from, id)));
	}
	
	private ACLMessage handleResult(ACLMessage msg, Result result) throws CodecException, OntologyException {
		GameInfo gameInfo	= games.get(msg.getConversationId());
		ACLMessage res		= msg.createReply();
		res.setPerformative(ACLMessage.REFUSE);
		
		if (gameInfo != null) {
			Long value = (Long) result.getValue();
			int remainder = gameInfo.getTotal() - value.intValue();
			// Find opponent
			AID next = msg.getSender().equals(gameInfo.getPlayerA()) ? gameInfo.getPlayerB() : gameInfo.getPlayerA();
			logger.debug(msg.getSender().getLocalName() + " took " + result.getValue() + ", " + next.getLocalName() + " is left with " + remainder);
			if (remainder > 1) {				
				gameInfo.setTotal(remainder);
				res = createDeltaRequest(next, msg.getConversationId());
			} else if (remainder == 1 || remainder == 0) {
				// Fire game-over event
				res = createGameOverEvent(remainder == 0 ? next : msg.getSender(), msg.getConversationId());
				// This game is over
				games.remove(msg.getConversationId());
				send(res);
				return null;
			} else {
				res.setContent(msg.getContent());
			}
		}
		
		return res;
	}
	
	/**
	 * RequestDelta requests a delta value.
	 * It asks player A or B in alternation until the game is over.
	 * Each game is tracked by a unique (conversation) ID.
	 * @see GetDelta
	 * @see GameOver
	 */
	private class RequestDelta extends SimpleAchieveREInitiator {
		
		public RequestDelta(Agent a, ACLMessage msg) {
			super(a, msg);
		}
		
		@Override
		protected void handleInform(ACLMessage msg) {
			ACLMessage reply = null;
			try {
				ContentElement ce = getContentManager().extractContent(msg);
				if (ce instanceof Result) {
					logger.debug("got result");
					reply = handleResult(msg, (Result)ce);
				} else {
					logger.warn("unexpected message");
				}
			} catch (Exception e) {
				logger.error("request delta failed", e);
				reply = msg.createReply();
				msg.setPerformative(ACLMessage.NOT_UNDERSTOOD);
			} finally {
				if (reply != null) {
					reset(reply);
				}
			}
		}
		
		@Override
		protected void handleAgree(ACLMessage msg) {
			logger.debug(msg.getSender().getLocalName() + " agreed");
		}
		
		@Override
		protected void handleRefuse(ACLMessage msg) {
			logger.warn(msg.getSender().getLocalName() + " refused");
		}
		
	}
	
	/**
	 * AwaitGame awaits incoming {@code start-game} requests.
	 * @see StartGame
	 */
	private class AwaitGame extends SimpleAchieveREResponder {

		public AwaitGame(Agent a, MessageTemplate mt) {
			super(a, mt);
		}
		
		@Override
		protected ACLMessage prepareResponse(ACLMessage request)
				throws NotUnderstoodException, RefuseException {
			
			try {
				ACLMessage res = request.createReply();
				Concept action = ((Action)getContentManager().extractContent(request)).getAction();
				if (action instanceof StartGame) {
					// Create a unique (game/conversation) ID
					String conversationId	= "game_" + System.currentTimeMillis(); 
					StartGame aStartGame	= (StartGame)action;
					
					// Store game info
					games.put(conversationId, aStartGame);
					
					logger.info("new game: " + 
							aStartGame.getPlayerA().getLocalName() + " vs. " +
							aStartGame.getPlayerB().getLocalName());
					
					// Ask first player for a delta value
					requestDelta(aStartGame.getPlayerA(), conversationId);
					
					// Inform sender that we agree on the game request
					res.setPerformative(ACLMessage.AGREE);
					return res;
				} else {
					throw new NotUnderstoodException("unkonw action");
				}
			} catch (Exception e) {
				logger.error("processing message", e);
				throw new NotUnderstoodException("not an action");
			} 
		}
		
	}

}
